The project provides a single API
endpoint to receive HTML
content and convert it to PDF
. It also offers options to add details to the generated PDF, such as:
This is achieved by utilising the headless version of the Chromium
browser.
This project is a refactor and upgrade of one I completed in May 2021. Originally written in Java (Spring Boot 2.4.4)
and using maven
as the package manager, it has undergone several improvements.
One key enhancement was the addition of a UI for OpenAPI
. However, before doing this, I needed to upgrade to Spring Boot 3
as a prerequisite.
Given the small size of my project, the upgrade process was relatively straightforward. For larger projects, significant changes may be required to complete the migration.
In my case, I simply updated the Spring Boot version in my pom.xml
file from 2.4.4
to 3.3.2
and made some minor adjustments to the project.
The most substantial change was updating the security configuration from this:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
// some code here
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//configure a simple AuthenticationManager with just a single user
auth.inMemoryAuthentication()
.passwordEncoder(passwordEncoder())
.withUser(this.username)
.password(passwordEncoder().encode(this.password))//the encoded password should be stored.
.roles("USER");
logger.info("username created with username: '{}' and password: '{}'", username, password);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.
authorizeRequests().anyRequest().authenticated() //secure all paths
.and()
.csrf().disable()
.httpBasic()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
}
To this, where a SecurityFilterChain
is incorporated:
@Configuration
public class SecurityConfig {
private final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
// some code here
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.
authorizeHttpRequests((request) ->
request.requestMatchers("/**")
.permitAll()
.anyRequest()
.authenticated()
)
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sessionMngConfig ->
sessionMngConfig.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
if (this.username != null && !this.username.isBlank() &&
this.password != null && !this.password.isBlank()) {
UserDetails user = User
.withUsername(this.username)
.password(passwordEncoder().encode(this.password))
.roles("USER")
.build();
logger.info("username created with username: '{}' and password: '{}'", username, password);
return new InMemoryUserDetailsManager(user);
}
else return new InMemoryUserDetailsManager();
}
}
As you can see in both examples, I’ve included an optional user
to add a security layer if required by the project. This is a basic security layer where the user
is stored only in memory. Its purpose is to restrict API access to trusted microservices or applications.
Another change I made was upgrading Java from version 11
to 22
, which was a straightforward update in my pom.xml
file as follows:
<properties>
<java.version>22</java.version>
</properties>
To add an OpenAPI UI to an existing Spring Boot
application, follow these steps:
Spring Boot
version is at least 3
and your Java
version is at least 17
.
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
@Operation(summary = "Convert HTML to PDF")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Successful response"),
@ApiResponse(responseCode = "400", description = "Bad request"),
})
Now, when you run the project, you can access Swagger UI
by navigating to /swagger-ui/index.html
. Here’s what it looks like:
Since Swagger UI
provides a better interface between the API
endpoints and the consumers, it’s crucial to make it as clear and understandable as possible.
For example, here is my request schema:
{
"bodyUrl": "string",
"bodyHtml": "string",
"bodyStyle": "string",
"cover": "string",
"header": "string",
"footer": "string",
"paperSize": "string",
"margin": "string",
"fontFamilyRTL": "string",
"fontFamilyLTR": "string",
"fontSizeRTL": "string",
"fontSizeLTR": "string",
"defaultDirection": "string",
"pageNumberPosition": "string",
"password": "string"
}
As you can see from the schema, the data type for paperSize
is string
, even though it only accepts A3
, A4
, A5
, and Letter
. It's better to define it as an enum
so that it reflects correctly in your Swagger UI
.
Another improvement I recommend is to make the enum
case-insensitive. Here’s how I’ve done it:
@Getter
public enum PaperSize {
A3,
A4,
A5,
Letter;
@JsonCreator
public static PaperSize fromValue(String value) {
for (PaperSize paperSize : PaperSize.values()) {
if (paperSize.toString().equalsIgnoreCase(value)) {
return paperSize;
}
}
throw new IllegalArgumentException("Invalid value: " + value + ". Valid values are: A3, A4, A5, and Letter");
}
}
By doing this, even if a user sends a4
or letter
instead of A4
or Letter
, it will still work correctly.
More importantly, when users go to the schema
tab in Swagger UI
, they can see a list of valid values. If they receive a Bad Request
response from the API, they can easily double-check the documentation.
This microservice uses the headless version of Chromium
to convert HTML
to PDF
, leveraging freely available tools to achieve this goal.
However, Chromium
alone isn’t sufficient to meet all the requirements. Additional features include:
PDF
.
PDF
.
I’ve achieved these features by utilising PDFBox
. Here is the dependency:
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.0</version>
</dependency>
Lastly, I added a Dockerfile
and published the project as a Docker
image on Docker Hub under the tag raminyavari/html2pdf:latest
.
Here is my Dockerfile
:
FROM alpine:latest
ARG BUILD_DATE
ARG VCS_REF
LABEL org.label-schema.build-date=$BUILD_DATE \
org.label-schema.description="Chrome running in headless mode in a tiny Alpine image" \
org.label-schema.name="alpine-chrome" \
org.label-schema.schema-version="1.0.0-rc1" \
org.label-schema.usage="https://github.com/Zenika/alpine-chrome/blob/master/README.md" \
org.label-schema.vcs-url="https://github.com/Zenika/alpine-chrome" \
org.label-schema.vcs-ref=$VCS_REF \
org.label-schema.vendor="Zenika" \
org.label-schema.version="latest"
COPY ./ /html2pdf
COPY ./docker/local.conf /etc/fonts/local.conf
RUN apk upgrade -U -a \
&& apk add \
openjdk22 \
maven \
&& rm -rf /var/cache/* \
&& mkdir /var/cache/apk
RUN cd /html2pdf \
&& mvn package -DskipTests \
&& cp /html2pdf/target/html2pdf-web-0.0.1-SNAPSHOT.jar /
RUN mkdir -p /usr/src/app \
&& adduser -D chrome \
&& chown -R chrome:chrome /usr/src/app
USER chrome
WORKDIR /usr/src/app
EXPOSE 8080
CMD java -jar /html2pdf-web-0.0.1-SNAPSHOT.jar